package educrypt.ciphers.hill;

import educrypt.commons.Alphabet;
import educrypt.commons.Text;
import educrypt.commons.util.MathUtil;
import educrypt.exceptions.EduCryptException;
import educrypt.i18n.Messages;
import educrypt.log.ObservableLogger;
import educrypt.log.SimpleLogger;

public class Hill {

	private static int MIN_KEYWORD_LENGTH = 4;

	/* STATIC METHODS */
	public static String formatKeyword(String keyword, Alphabet alphabet)
			throws EduCryptException {
		keyword = keyword.toLowerCase();

		// Remove unknown symbols
		keyword = alphabet.filterKnownSymbols(keyword);

		// Truncate keyword to a number which its square root is a natural
		// number
		int keywordLength = getValidKeywordLength(keyword);
		keyword = keyword.substring(0, keywordLength);

		// If matrix is invertible the key is valid, if not an exception will be
		// thrown
		int[] numericKeyword = alphabet.getSymbolsPosition(keyword);
		new HillKeyMatrix(numericKeyword, alphabet.size());

		return keyword;

	}

	private static int getValidKeywordLength(String keyword)
			throws EduCryptException {
		int length = keyword.length();
		boolean validLengthFound = false;
		while (length >= MIN_KEYWORD_LENGTH && !validLengthFound) {
			validLengthFound = isValidKeywordLength(length);
			if (!validLengthFound) {
				length--;
			}
		}
		if (!validLengthFound) {
			throw new EduCryptException(Messages.getString("errInvalidKeyword"));
		}
		return length;
	}

	private static boolean isValidKeyword(String keyword, Alphabet alphabet) {
		return alphabet.testBelonging(keyword)
				|| isValidKeywordLength(keyword.length());
	}

	private static boolean isValidKeywordLength(int length) {
		double rowLength = Math.sqrt(length);
		return MathUtil.isNatural(rowLength);
	}

	/* END STATIC METHODS */

	private Alphabet alphabet;
	private HillKeyMatrix key;

	public Hill(Alphabet alphabet, String keyword) throws EduCryptException {
		this.alphabet = alphabet;
		if (!isValidKeyword(keyword, alphabet)) {
			throw new EduCryptException(Messages.getString("errInvalidKeyword"));
		}

		int[] numericKeyword = alphabet.getSymbolsPosition(keyword);
		int modularBase = alphabet.size();
		this.key = new HillKeyMatrix(numericKeyword, modularBase);

		this.logCipherDetails(keyword);
	}

	public Hill(Alphabet alphabet, HillKeyMatrix key) {
		this.alphabet = alphabet;
		this.key = key;
	}

	private void logCipherDetails(String keyword) {
		// Log to console
		SimpleLogger logger = ObservableLogger.getInstance();
		logger.log(SimpleLogger.TITLE,
				"---- " + Messages.getString("cipherHill") + " ----");

		logger.log(SimpleLogger.TITLE, Messages.getString("guiAlphabet"));
		logger.log(SimpleLogger.CONTENT, alphabet.getSymbols());

		logger.log(SimpleLogger.TITLE, Messages.getString("guiKeyword"));
		logger.log(SimpleLogger.CONTENT, keyword);

		logger.log(SimpleLogger.TITLE,
				Messages.getString("guiGeneratedKeyMatrix"));
		logger.log(SimpleLogger.CONTENT, key.toString());
	}

	public Text encode(Text plainText) {
		String cipherText = this
				.applyHillAlgorithm(plainText.getContent(), key);
		return new Text(cipherText);
	}

	public Text decode(Text cipherText) {
		String plainText = this.applyHillAlgorithm(cipherText.getContent(),
				key.inverse());
		return new Text(plainText);
	}

	private String applyHillAlgorithm(String textToTransform, HillKeyMatrix key) {
		StringBuffer transformedText = new StringBuffer(
				textToTransform.length());
		int ngramSize = key.getRowLength();
		HillNgrams ngrams = new HillNgrams(textToTransform, alphabet, ngramSize);
		for (int i = 0; i < ngrams.size(); i++) {
			HillNgram cipherNgram = ngrams.get(i);
			String transformedRealNgram = transformNgram(
					cipherNgram.getRealNgram(), key);
			transformedText.append(cipherNgram
					.getTransformedRawNgram(transformedRealNgram));
		}
		return transformedText.toString();
	}

	private String transformNgram(String plainRealNgram, HillKeyMatrix key) {
		int[] vectorToCipher = this.alphabet.getSymbolsPosition(plainRealNgram);
		int[] cipherVector = key.multiplyByVector(vectorToCipher);
		String cipherNgram = this.alphabet.symbolsAt(cipherVector);
		return cipherNgram;
	}

}
